package net.csiit.platform.codeguard.maven.plugin;

import net.csiit.platform.codeguard.tools.Constants;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;

import java.io.*;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;

/**
 * encrypt jar for spring boot application
 *
 * @author adam
 */
@Mojo(name = "codeGuard4Boot", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true)
public class CodeGuard4BootMojo extends AbstractGuardPackageMojo {

    /**
     * encrypt key
     */
    @Parameter(defaultValue = "${codeGuard4Boot.key}")
    private String key;
    @Parameter(defaultValue = "${codeGuard4Boot.jarFileName}")
    private String jarFileName;
    @Parameter(defaultValue = "${codeGuard4Boot.postfix}")
    private String postfix;
    @Parameter(defaultValue = "${codeGuard4Boot.encryptBootInfClass}")
    private boolean encryptBootInfClass = true;
    @Parameter(defaultValue = "${codeGuard.encryptPackages}")
    private List<String> encryptPackages;
    /**
     * encrypt jar file name pattern.
     *
     * <encryptJarFiles>
     * <param>test1.jar</param>
     * <param>test2.jar</param>
     * <param>t.*.jar</param>
     * </encryptJarFiles>
     */
    @Parameter(defaultValue = "${codeGuard4Boot.encryptJarFiles}")
    private List<String> encryptJarFiles;


    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        if (StringUtils.length(key) != Constants.AES_KEY_LENGTH) {
            getLog().error("key must has 16 characters !");
            throw new MojoExecutionException("key must has 16 characters !");
        }

        if (encryptJarFiles == null || encryptJarFiles.size() == 0) {
            getLog().warn("[code-guard-4-boot] No encrypt jar files specified, plugin will not run !");
            return;
        }

        try (JarFile jarFile = getJarFile(target, jarFileName)) {
            if (jarFile == null) {
                throw new MojoExecutionException("can not found target jar file : " + jarFileName);
            }

            getLog().info("[code-guard-4-boot] begin to encrypt spring boot jar file : " + jarFile.getName());
            doHandleJarFile(jarFile, true);
            getLog().info("[code-guard-4-boot] encrypt spring boot jar file : " + jarFile.getName() + "  success !");
        } catch (MojoExecutionException e) {
            throw e;
        } catch (Exception e) {
            getLog().error(e.getMessage(), e);
            throw new MojoExecutionException(e.getMessage(), e);
        }
    }

    /**
     * copy spring boot jar entry to target jar file
     *
     * @param jarFile source jar file, spring boot generated jar file
     * @param encrypt true for encrypt, false for decrypt
     * @return target jar file
     */
    public File doHandleJarFile(JarFile jarFile, boolean encrypt) throws IOException {
        if (StringUtils.isEmpty(postfix)) {
            postfix = DEFAULT_POSTFIX;
        }

        String fileName = jarFile.getName().substring(jarFile.getName().lastIndexOf(File.separator) + 1);
        File targetFile = new File(target.getAbsolutePath(), fileName.replace(".jar", "").concat(".").concat(postfix).concat(".jar"));

        Manifest targetManifest = new Manifest(jarFile.getManifest());
        targetManifest.getMainAttributes().putValue(Constants.GUARD_MANIFEST_ATTR, Constants.GUARD_MANIFEST_BOOT_VALUE);

        Enumeration<JarEntry> enumeration = jarFile.entries();
        try (OutputStream outputStream = new FileOutputStream(targetFile);
             JarOutputStream jarOutputStream = new JarOutputStream(outputStream, targetManifest)) {
            while (enumeration.hasMoreElements()) {
                JarEntry jarEntry = enumeration.nextElement();
                copyBootJarEntry(jarFile, jarEntry, jarOutputStream, encrypt);
            }
        }

        return targetFile;
    }

    /**
     * do the copy jar entry operation
     *
     * @param jarFile         source jar file
     * @param jarEntry        the entry to be copied
     * @param jarOutputStream target output stream
     * @param encrypt         true for encrypt, false for decrypt
     */
    private void copyBootJarEntry(JarFile jarFile, JarEntry jarEntry, JarOutputStream jarOutputStream, boolean encrypt) throws IOException {
        String name = jarEntry.getName();
        if (StringUtils.startsWith(name, "META-INF")) {
            //don't copy meta info
            return;
        }

        //jar file
        if (StringUtils.startsWith(name, "BOOT-INF/lib") && StringUtils.endsWith(name, ".jar")) {
            String jarName = name.substring(name.lastIndexOf("/") + 1);
            for (String pattern : encryptJarFiles) {
                if (StringUtils.equals(jarName, pattern) || jarName.matches(pattern)) {
                    copyJarFileInEntry(jarFile, jarEntry, jarName, jarOutputStream, encrypt);
                    return;
                }
            }
        }

        if (encryptBootInfClass && StringUtils.startsWith(name, "BOOT-INF/classes") && StringUtils.endsWith(name, ".class")) {
            copyJarEntry(jarFile, jarEntry, key, jarOutputStream, encryptPackages, encrypt);
            return;
        }

        // if not specified jar file, then copy as it is
        JarEntry targetEntry = new JarEntry(new ZipEntry(jarEntry));
        try (InputStream inputStream = jarFile.getInputStream(jarEntry)) {
            addInputStreamEntry(inputStream, jarOutputStream, targetEntry);
        }
    }

    /**
     * If the JarEntry is jar file, then extract the jar file to target directory and encrypt(or decrypt) it, then add the dest jar file to jar output stream
     *
     * @param jarFile         original jar file
     * @param jarEntry        jar entry tobe handled
     * @param jarName         simple jar file name
     * @param jarOutputStream target output stream
     * @param encrypt         true for encrypt, false for decrypt
     */
    private void copyJarFileInEntry(JarFile jarFile, JarEntry jarEntry, String jarName, JarOutputStream jarOutputStream, boolean encrypt) throws IOException {
        getLog().info("[code-guard-4-boot] handle target jar : " + jarName);

        File tempJarFile = createTempTargetFile(getGuardWorkspace().getAbsolutePath(), jarName);
        try (InputStream inputStream = jarFile.getInputStream(jarEntry);
             OutputStream outputStream = new FileOutputStream(tempJarFile)) {
            IOUtils.copy(inputStream, outputStream);
        }

        try (JarFile sourceTempJarFile = new JarFile(tempJarFile, true)) {
            File targetJarFile = handleJarFile(sourceTempJarFile, key, postfix, encryptPackages, encrypt);
            if (targetJarFile == null) {
                getLog().error("[code-guard-4-boot] can not handle jar file " + jarName);
                return;
            }

            JarEntry targetEntry = new JarEntry(jarEntry.getName().replace(jarName, "").concat(targetJarFile.getName()));
            targetEntry.setTime(jarEntry.getTime());
            targetEntry.setComment(jarEntry.getComment());
            targetEntry.setMethod(jarEntry.getMethod());
            targetEntry.setSize(targetJarFile.length());
            if (targetEntry.getMethod() == JarEntry.STORED) {
                targetEntry.setCrc(getCrc32(targetJarFile));
            }

            addEntry(targetJarFile, jarOutputStream, targetEntry);

            targetJarFile.delete();
            targetJarFile.deleteOnExit();
        }

        tempJarFile.delete();
        tempJarFile.deleteOnExit();
    }

    private long getCrc32(File file) throws IOException {
        try (InputStream inputStream = new FileInputStream(file)) {
            byte[] bytes = IOUtils.readFully(inputStream, inputStream.available());
            CRC32 crc32 = new CRC32();
            crc32.update(bytes);
            return crc32.getValue();
        }
    }
}
